package pl.elka.pszt.model.solver;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import pl.elka.pszt.model.Assigned;
import pl.elka.pszt.model.Knowledge;
import pl.elka.pszt.model.KnowledgeElement;
import pl.elka.pszt.model.Model;
import pl.elka.pszt.utils.Log;

/**
 * Klasa zajmująca się rozwiązywaniem wnioskowania. Do dzialania potrzebuje bazy
 * wiedzy oraz zapytania. Opcjonalnie mozna podac liczbe watkow - domyslnie
 * uzywa tylko jednego. <br>
 * <br>
 * Najpierw ustawiamy baze wiedzy metoda:
 * <code>public void setKnowledge(Knowledge knowledge)</code>, nastepnie
 * pytanie: <code>setQuestion(Assigned question)</code>, po czym wywolujemy
 * metode: <code>public void solve()</code>.
 * 
 * @author Andrzej Makarewicz
 * 
 */
public class DefaultSolver implements Solver, Reasoning.ReasoningListener {
	
	private static final String TAG = DefaultSolver.class.getSimpleName();
	
	private Knowledge knowledge;
	private Assigned question;
	private int threadsNumber = 1;
	private ExecutorService executor;
	private Model model;
	//
	private volatile boolean isSolving, isClosed;
	
	public DefaultSolver(Model model) {
		this.model = model;
		executor = createExecutor(threadsNumber);
	}
	
	/**
	 * 
	 * @param threadsNumber
	 * @return wielowatkowy {@link ThreadPoolExecutor}
	 */
	private static ThreadPoolExecutor createExecutor(int threadsNumber) {
		return new ThreadPoolExecutor(threadsNumber, threadsNumber, 0L, TimeUnit.MILLISECONDS,
				new LinkedBlockingQueue<Runnable>(), new SolverThreadFactory(), new SolverRejectedExecutionHandler());
	}
	
	@Override
	public void setKnowledge(Knowledge knowledge) {
		if (knowledge == null) throw new NullPointerException();
		if (isSolving() || isClosed()) throw new IllegalStateException();
		
		this.knowledge = knowledge;
	}
	
	@Override
	public Knowledge getKnowledge() {
		return knowledge;
	}
	
	@Override
	public void setQuestion(Assigned question) {
		if (question == null) throw new NullPointerException();
		if (isSolving() || isClosed()) throw new IllegalStateException();
		
		this.question = question;
	}
	
	@Override
	public Assigned getQuestion() {
		return question;
	}
	
	@Override
	public void setThreadsNumber(int threadsNumber) {
		if (isSolving() || isClosed()) throw new IllegalStateException();
		
		if (threadsNumber < 1) throw new IllegalArgumentException();
		
		if (this.threadsNumber != threadsNumber) {
			this.threadsNumber = threadsNumber;
			executor.shutdownNow();
			executor = createExecutor(threadsNumber);
		}
	}
	
	@Override
	public int getThreadsNumber() {
		return threadsNumber;
	}
	
	@Override
	public void solve() {
		//Log.o(TAG, Log.getCurrentMethodName());
		
		if (isSolving() || isClosed()) throw new IllegalStateException();
		
		isSolving = true;
		solvingTime = System.currentTimeMillis();
		solverFutures = new ArrayList<Future>(threadsNumber);
		for (int i = 0; i < threadsNumber; i++) {
			Runnable runnable = Reasoning.createReasoningRunnable(knowledge, question, this);
			solverFutures.add(executor.submit(runnable));
		}
	}
	
	private void cancelRunningSolvers() {
		for (Future f : solverFutures) {
			f.cancel(true);
		}
	}
	
	private long solvingTime;
	private List<Future> solverFutures;
	
	/**
	 * Wywolywane przez jeden z watkow, jezeli zakonczy dzialanie. Reszta watkow
	 * jest wtedy zatrzymywana, a wynik podawany do modelu.
	 */
	@Override
	public synchronized void onReasoningFinished(boolean success, List<KnowledgeElement> k) {
		if (!isSolving()) return;
		
		isSolving = false;
		solvingTime = System.currentTimeMillis() - solvingTime;
		cancelRunningSolvers();
		
		model.onReasoningFinished(success, k, solvingTime);
	}
	
	@Override
	public boolean isSolving() {
		return isSolving;
	}
	
	@Override
	public boolean isClosed() {
		return isClosed;
	}
	
	/**
	 * Zamyka solver zatrzymujac executor.
	 */
	@Override
	public void close() {
		isClosed = true;
		isSolving = false;
		if (executor != null) {
			executor.shutdownNow();
			executor = null;
		}
		knowledge = null;
		question = null;
		threadsNumber = 0;
	}
	
	@Override
	protected void finalize() throws Throwable {
		try {
			if (!isClosed()) close();
		}
		finally {
			super.finalize();
		}
	}
	
	/**
	 * 
	 * Generuje watki o normalnym priorytecie.
	 * 
	 */
	private static class SolverThreadFactory implements ThreadFactory {
		
		private final AtomicInteger threadId = new AtomicInteger(1);
		
		@Override
		public Thread newThread(Runnable runnable) {
			String name = String.format("Solver thread: %d", threadId.getAndIncrement());
			Thread t = new Thread(runnable, name);
			t.setPriority(Thread.NORM_PRIORITY);
			return t;
		}
	}
	
	private static class SolverRejectedExecutionHandler implements RejectedExecutionHandler {
		
		@Override
		public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
			Log.e(TAG, "Runnable " + runnable + " was rejected by executor.");
		}
		
	}
}
